#!/usr/bin/perl -T
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.

use 5.14.0;
use strict;
use warnings;

use lib qw(. lib local/lib/perl5);

use Bugzilla;
use Bugzilla::Constants;
use Bugzilla::Flag;
use Bugzilla::FlagType;
use Bugzilla::Group;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Product;
use Bugzilla::Token;

# Make sure the user is logged in and has the right privileges.
my $user     = Bugzilla->login(LOGIN_REQUIRED);
my $cgi      = Bugzilla->cgi;
my $template = Bugzilla->template;

print $cgi->header();

$user->in_group('editcomponents')
  || scalar(@{$user->get_products_by_permission('editcomponents')})
  || ThrowUserError("auth_failure",
  {group => "editcomponents", action => "edit", object => "flagtypes"});

# We need this everywhere.
my $vars     = get_products_and_components();
my @products = @{$vars->{products}};

my $action    = $cgi->param('action') || 'list';
my $token     = $cgi->param('token');
my $prod_name = $cgi->param('product');
my $comp_name = $cgi->param('component');
my $flag_id   = $cgi->param('id');

my ($product, $component);

if ($prod_name) {

  # Make sure the user is allowed to view this product name.
  # Users with global editcomponents privs can see all product names.
  ($product) = grep { lc($_->name) eq lc($prod_name) } @products;
  $product || ThrowUserError('product_access_denied', {name => $prod_name});
}

if ($comp_name) {
  $product || ThrowUserError('flag_type_component_without_product');
  ($component) = grep { lc($_->name) eq lc($comp_name) } @{$product->components};
  $component
    || ThrowUserError('product_unknown_component',
    {product => $product->name, comp => $comp_name});
}

# If 'categoryAction' is set, it has priority over 'action'.
if (my ($category_action)
  = grep { $_ =~ /^categoryAction-(?:\w+)$/ } $cgi->multi_param())
{
  $category_action =~ s/^categoryAction-//;

  my @inclusions = $cgi->multi_param('inclusions');
  my @exclusions = $cgi->multi_param('exclusions');
  my @categories;
  if ($category_action =~ /^(in|ex)clude$/) {
    if (!$user->in_group('editcomponents') && !$product) {

      # The user can only add the flag type to products they can administrate.
      foreach my $prod (@products) {
        push(@categories, $prod->id . ':0');
      }
    }
    else {
      my $category
        = ($product ? $product->id : 0) . ':' . ($component ? $component->id : 0);
      push(@categories, $category);
    }
  }

  if ($category_action eq 'include') {
    foreach my $category (@categories) {
      push(@inclusions, $category) unless grep($_ eq $category, @inclusions);
    }
  }
  elsif ($category_action eq 'exclude') {
    foreach my $category (@categories) {
      push(@exclusions, $category) unless grep($_ eq $category, @exclusions);
    }
  }
  elsif ($category_action eq 'removeInclusion') {
    my @inclusion_to_remove = $cgi->multi_param('inclusion_to_remove');
    foreach my $remove (@inclusion_to_remove) {
      @inclusions = grep { $_ ne $remove } @inclusions;
    }
  }
  elsif ($category_action eq 'removeExclusion') {
    my @exclusion_to_remove = $cgi->multi_param('exclusion_to_remove');
    foreach my $remove (@exclusion_to_remove) {
      @exclusions = grep { $_ ne $remove } @exclusions;
    }
  }

  $vars->{'groups'} = get_settable_groups();
  $vars->{'action'} = $action;

  my $type = {};
  $type->{$_} = $cgi->param($_) foreach $cgi->param();

  # Make sure boolean fields are defined, else they fall back to 1.
  foreach
    my $boolean (qw(is_active is_requestable is_requesteeble is_multiplicable))
  {
    $type->{$boolean} ||= 0;
  }

  # That's what I call a big hack. The template expects to see a group object.
  $type->{'grant_group'}             = {};
  $type->{'grant_group'}->{'name'}   = $cgi->param('grant_group');
  $type->{'request_group'}           = {};
  $type->{'request_group'}->{'name'} = $cgi->param('request_group');

  $vars->{'inclusions'} = clusion_array_to_hash(\@inclusions, \@products);
  $vars->{'exclusions'} = clusion_array_to_hash(\@exclusions, \@products);

  $vars->{'type'}           = $type;
  $vars->{'token'}          = $token;
  $vars->{'check_clusions'} = 1;
  $vars->{'can_fully_edit'} = $cgi->param('can_fully_edit');

  $template->process("admin/flag-type/edit.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
  exit;
}

if ($action eq 'list') {
  my $product_id       = $product                        ? $product->id   : 0;
  my $component_id     = $component                      ? $component->id : 0;
  my $show_flag_counts = $cgi->param('show_flag_counts') ? 1              : 0;
  my $group_id         = $cgi->param('group');
  if ($group_id) {
    detaint_natural($group_id) || ThrowUserError('invalid_group_ID');
  }

  my $bug_flagtypes;
  my $attach_flagtypes;

  # If a component is given, restrict the list to flag types available
  # for this component.
  if ($component) {
    $bug_flagtypes    = $component->flag_types->{'bug'};
    $attach_flagtypes = $component->flag_types->{'attachment'};

    # Filter flag types if a group ID is given.
    $bug_flagtypes    = filter_group($bug_flagtypes,    $group_id);
    $attach_flagtypes = filter_group($attach_flagtypes, $group_id);

  }

  # If only a product is specified but no component, then restrict the list
  # to flag types available in at least one component of that product.
  elsif ($product) {
    $bug_flagtypes    = $product->flag_types->{'bug'};
    $attach_flagtypes = $product->flag_types->{'attachment'};

    # Filter flag types if a group ID is given.
    $bug_flagtypes    = filter_group($bug_flagtypes,    $group_id);
    $attach_flagtypes = filter_group($attach_flagtypes, $group_id);
  }

  # If no product is given, then show all flag types available.
  else {
    my $flagtypes = get_editable_flagtypes(\@products, $group_id);
    $bug_flagtypes    = [grep { $_->target_type eq 'bug' } @$flagtypes];
    $attach_flagtypes = [grep { $_->target_type eq 'attachment' } @$flagtypes];
  }

  if ($show_flag_counts) {
    my %bug_lists;
    my %map = ('+' => 'granted', '-' => 'denied', '?' => 'pending');

    foreach my $flagtype (@$bug_flagtypes, @$attach_flagtypes) {
      $bug_lists{$flagtype->id} = {};
      my $flags = Bugzilla::Flag->match({type_id => $flagtype->id});

      # Build lists of bugs, triaged by flag status.
      push(@{$bug_lists{$flagtype->id}->{$map{$_->status}}}, $_->bug_id)
        foreach @$flags;
    }
    $vars->{'bug_lists'}        = \%bug_lists;
    $vars->{'show_flag_counts'} = 1;
  }

  $vars->{'selected_product'}   = $product   ? $product->name   : '';
  $vars->{'selected_component'} = $component ? $component->name : '';
  $vars->{'bug_types'}          = $bug_flagtypes;
  $vars->{'attachment_types'}   = $attach_flagtypes;

  $template->process("admin/flag-type/list.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
  exit;
}

if ($action eq 'enter') {
  my $type = $cgi->param('target_type');
  ($type eq 'bug' || $type eq 'attachment')
    || ThrowCodeError('flag_type_target_type_invalid', {target_type => $type});

  $vars->{'action'} = 'insert';
  $vars->{'token'}  = issue_session_token('add_flagtype');
  $vars->{'type'}   = {'target_type' => $type};

  # Only users with global editcomponents privs can add a flagtype
  # to all products.
  $vars->{'inclusions'} = {'__Any__:__Any__' => '0:0'}
    if $user->in_group('editcomponents');
  $vars->{'can_fully_edit'} = 1;

  # Get a list of groups available to restrict this flag type against.
  $vars->{'groups'} = get_settable_groups();

  $template->process("admin/flag-type/edit.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
  exit;
}

if ($action eq 'edit' || $action eq 'copy') {
  my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
  $vars->{'type'}           = $flagtype;
  $vars->{'can_fully_edit'} = $can_fully_edit;

  if ($user->in_group('editcomponents')) {
    $vars->{'inclusions'} = $flagtype->inclusions;
    $vars->{'exclusions'} = $flagtype->exclusions;
  }
  else {
    # Filter products the user shouldn't know about.
    $vars->{'inclusions'}
      = clusion_array_to_hash([values %{$flagtype->inclusions}], \@products);
    $vars->{'exclusions'}
      = clusion_array_to_hash([values %{$flagtype->exclusions}], \@products);
  }

  if ($action eq 'copy') {
    $vars->{'action'} = "insert";
    $vars->{'token'}  = issue_session_token('add_flagtype');
  }
  else {
    $vars->{'action'} = "update";
    $vars->{'token'}  = issue_session_token('edit_flagtype');
  }

  # Get a list of groups available to restrict this flag type against.
  $vars->{'groups'} = get_settable_groups();

  $template->process("admin/flag-type/edit.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
  exit;
}

if ($action eq 'insert') {
  check_token_data($token, 'add_flagtype');

  my $name             = $cgi->param('name');
  my $description      = $cgi->param('description');
  my $target_type      = $cgi->param('target_type');
  my $cc_list          = $cgi->param('cc_list');
  my $sortkey          = $cgi->param('sortkey');
  my $is_active        = $cgi->param('is_active');
  my $is_requestable   = $cgi->param('is_requestable');
  my $is_specifically  = $cgi->param('is_requesteeble');
  my $is_multiplicable = $cgi->param('is_multiplicable');
  my $grant_group      = $cgi->param('grant_group');
  my $request_group    = $cgi->param('request_group');
  my @inclusions       = $cgi->multi_param('inclusions');
  my @exclusions       = $cgi->multi_param('exclusions');

  # Filter inclusion and exclusion lists to products the user can see.
  unless ($user->in_group('editcomponents')) {
    @inclusions = values %{clusion_array_to_hash(\@inclusions, \@products)};
    @exclusions = values %{clusion_array_to_hash(\@exclusions, \@products)};
  }

  my $flagtype = Bugzilla::FlagType->create({
    name             => $name,
    description      => $description,
    target_type      => $target_type,
    cc_list          => $cc_list,
    sortkey          => $sortkey,
    is_active        => $is_active,
    is_requestable   => $is_requestable,
    is_requesteeble  => $is_specifically,
    is_multiplicable => $is_multiplicable,
    grant_group      => $grant_group,
    request_group    => $request_group,
    inclusions       => \@inclusions,
    exclusions       => \@exclusions
  });

  delete_token($token);

  $vars->{'name'}    = $flagtype->name;
  $vars->{'message'} = "flag_type_created";

  my $flagtypes = get_editable_flagtypes(\@products);
  $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @$flagtypes];
  $vars->{'attachment_types'}
    = [grep { $_->target_type eq 'attachment' } @$flagtypes];

  $template->process("admin/flag-type/list.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
  exit;
}

if ($action eq 'update') {
  check_token_data($token, 'edit_flagtype');

  my $name             = $cgi->param('name');
  my $description      = $cgi->param('description');
  my $cc_list          = $cgi->param('cc_list');
  my $sortkey          = $cgi->param('sortkey');
  my $is_active        = $cgi->param('is_active');
  my $is_requestable   = $cgi->param('is_requestable');
  my $is_specifically  = $cgi->param('is_requesteeble');
  my $is_multiplicable = $cgi->param('is_multiplicable');
  my $grant_group      = $cgi->param('grant_group');
  my $request_group    = $cgi->param('request_group');
  my @inclusions       = $cgi->multi_param('inclusions');
  my @exclusions       = $cgi->multi_param('exclusions');

  my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
  if ($cgi->param('check_clusions') && !$user->in_group('editcomponents')) {

    # Filter inclusion and exclusion lists to products the user can edit.
    @inclusions = values %{clusion_array_to_hash(\@inclusions, \@products)};
    @exclusions = values %{clusion_array_to_hash(\@exclusions, \@products)};

    # Bring back the products the user cannot edit.
    foreach my $item (values %{$flagtype->inclusions}) {
      my ($prod_id, $comp_id) = split(':', $item);
      push(@inclusions, $item) unless grep { $_->id == $prod_id } @products;
    }
    foreach my $item (values %{$flagtype->exclusions}) {
      my ($prod_id, $comp_id) = split(':', $item);
      push(@exclusions, $item) unless grep { $_->id == $prod_id } @products;
    }
  }

  if ($can_fully_edit) {
    $flagtype->set_name($name);
    $flagtype->set_description($description);
    $flagtype->set_cc_list($cc_list);
    $flagtype->set_sortkey($sortkey);
    $flagtype->set_is_active($is_active);
    $flagtype->set_is_requestable($is_requestable);
    $flagtype->set_is_specifically_requestable($is_specifically);
    $flagtype->set_is_multiplicable($is_multiplicable);
    $flagtype->set_grant_group($grant_group);
    $flagtype->set_request_group($request_group);
  }
  $flagtype->set_clusions(
    {inclusions => \@inclusions, exclusions => \@exclusions})
    if $cgi->param('check_clusions');
  my $changes = $flagtype->update();

  delete_token($token);

  $vars->{'flagtype'} = $flagtype;
  $vars->{'changes'}  = $changes;
  $vars->{'message'}  = 'flag_type_updated';

  my $flagtypes = get_editable_flagtypes(\@products);
  $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @$flagtypes];
  $vars->{'attachment_types'}
    = [grep { $_->target_type eq 'attachment' } @$flagtypes];

  $template->process("admin/flag-type/list.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
  exit;
}

if ($action eq 'confirmdelete') {
  my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
  ThrowUserError('flag_type_cannot_delete', {flagtype => $flagtype})
    unless $can_fully_edit;

  $vars->{'flag_type'} = $flagtype;
  $vars->{'token'}     = issue_session_token('delete_flagtype');

  $template->process("admin/flag-type/confirm-delete.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
  exit;
}

if ($action eq 'delete') {
  check_token_data($token, 'delete_flagtype');

  my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
  ThrowUserError('flag_type_cannot_delete', {flagtype => $flagtype})
    unless $can_fully_edit;

  $flagtype->remove_from_db();

  delete_token($token);

  $vars->{'name'}    = $flagtype->name;
  $vars->{'message'} = "flag_type_deleted";

  my @flagtypes = Bugzilla::FlagType->get_all;
  $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @flagtypes];
  $vars->{'attachment_types'}
    = [grep { $_->target_type eq 'attachment' } @flagtypes];

  $template->process("admin/flag-type/list.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
  exit;
}

if ($action eq 'deactivate') {
  check_token_data($token, 'delete_flagtype');

  my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id);
  ThrowUserError('flag_type_cannot_deactivate', {flagtype => $flagtype})
    unless $can_fully_edit;

  $flagtype->set_is_active(0);
  $flagtype->update();

  delete_token($token);

  $vars->{'message'}   = "flag_type_deactivated";
  $vars->{'flag_type'} = $flagtype;

  my @flagtypes = Bugzilla::FlagType->get_all;
  $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @flagtypes];
  $vars->{'attachment_types'}
    = [grep { $_->target_type eq 'attachment' } @flagtypes];

  $template->process("admin/flag-type/list.html.tmpl", $vars)
    || ThrowTemplateError($template->error());
  exit;
}

ThrowUserError('unknown_action', {action => $action});

#####################
# Helper subroutines
#####################

sub get_products_and_components {
  my $vars = {};
  my $user = Bugzilla->user;

  my @products;
  if ($user->in_group('editcomponents')) {
    if (Bugzilla->params->{useclassification}) {

      # We want products grouped by classifications.
      @products = map { @{$_->products} } Bugzilla::Classification->get_all;
    }
    else {
      @products = Bugzilla::Product->get_all;
    }
  }
  else {
    @products = @{$user->get_products_by_permission('editcomponents')};

    if (Bugzilla->params->{useclassification}) {
      my %class;
      push(@{$class{$_->classification_id}}, $_) foreach @products;

      # Let's sort the list by classifications.
      @products = ();
      push(@products, @{$class{$_->id} || []})
        foreach Bugzilla::Classification->get_all;
    }
  }

  my %components;
  foreach my $product (@products) {
    $components{$_->name} = 1 foreach @{$product->components};
  }
  $vars->{'products'}   = \@products;
  $vars->{'components'} = [sort(keys %components)];
  return $vars;
}

sub get_editable_flagtypes {
  my ($products, $group_id) = @_;
  my $flagtypes;

  if (Bugzilla->user->in_group('editcomponents')) {
    $flagtypes = Bugzilla::FlagType::match({group => $group_id});
    return $flagtypes;
  }

  my %visible_flagtypes;
  foreach my $product (@$products) {
    foreach my $target ('bug', 'attachment') {
      my $prod_flagtypes = $product->flag_types->{$target};
      $visible_flagtypes{$_->id} ||= $_ foreach @$prod_flagtypes;
    }
  }
  @$flagtypes = sort { $a->sortkey <=> $b->sortkey || $a->name cmp $b->name }
    values %visible_flagtypes;

  # Filter flag types if a group ID is given.
  $flagtypes = filter_group($flagtypes, $group_id);
  return $flagtypes;
}

sub get_settable_groups {
  my $user = Bugzilla->user;
  my $groups
    = $user->in_group('editcomponents')
    ? [Bugzilla::Group->get_all]
    : $user->groups;
  return $groups;
}

sub filter_group {
  my ($flag_types, $gid) = @_;
  return $flag_types unless $gid;

  my @flag_types = grep {
         ($_->grant_group && $_->grant_group->id == $gid)
      || ($_->request_group && $_->request_group->id == $gid)
  } @$flag_types;

  return \@flag_types;
}

# Convert the array @clusions('prod_ID:comp_ID') back to a hash of
# the form %clusions{'prod_name:comp_name'} = 'prod_ID:comp_ID'
sub clusion_array_to_hash {
  my ($array, $visible_products) = @_;
  my $user      = Bugzilla->user;
  my $has_privs = $user->in_group('editcomponents');

  my %hash;
  my %products;
  my %components;

  foreach my $ids (@$array) {
    my ($product_id, $component_id) = split(":", $ids);
    my $product_name   = "__Any__";
    my $component_name = "__Any__";

    if ($product_id) {
      ($products{$product_id}) = grep { $_->id == $product_id } @$visible_products;
      next unless $products{$product_id};
      $product_name = $products{$product_id}->name;

      if ($component_id) {
        ($components{$component_id})
          = grep { $_->id == $component_id } @{$products{$product_id}->components};
        next unless $components{$component_id};
        $component_name = $components{$component_id}->name;
      }
    }
    else {
      # Users with local editcomponents privs cannot use __Any__:__Any__.
      next unless $has_privs;

      # It's illegal to select a component without a product.
      next if $component_id;
    }
    $hash{"$product_name:$component_name"} = $ids;
  }
  return \%hash;
}
